package com.cocotingo.snail;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.VolatileImage;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

import com.cocotingo.snail.dispatcher.Dispatcher;


public class ViewContext {
	
	//// Static ////
	
	static class Driver implements Runnable {
		private final List<ViewContext> viewContexts = new ArrayList<ViewContext>();
		private final List<Animation> animations = new LinkedList<Animation>();
		private boolean running;
		int fps;
		int max_fps;
		public Driver() {
			running = false;
			max_fps = 60;
		}
		public void registerContext(ViewContext w) {
			if (!viewContexts.contains(w)) {
				viewContexts.add(w);
				if (running == false) {
					running = true;
					Dispatcher.execute(this);
				}
			}
		}
		public void unregisterContext(ViewContext w) {
			if (viewContexts.contains(w)) {
				if (w.graphics != null) {
					w.graphics.dispose();
					w.graphics = null;
				}
				if (w.smallImage != null) {
					w.smallImage.flush();
					w.smallImage = null;
				}
				w.bufferPool.clearBuffer();
				viewContexts.remove(w);
				if (viewContexts.size() == 0) {
					running = false;
					if (sharedGraphics != null) {
						sharedGraphics.dispose();
						sharedGraphics = null;
					}
					if (sharedSmallImage != null) {
						sharedSmallImage.flush();
						sharedSmallImage = null;
					}
				}
			}
		}
		public void registerAnimation(Animation a) {
			animations.add(a);
		}
		public void unregisterAnimation(Animation a) {
			animations.remove(a);
		}
		@Override
		public void run() {
			class Timer {
				long t0 = System.currentTimeMillis();
				long t, dt;
			}
			final Timer timer = new Timer();
			final Runnable runnable = new Runnable() {
				@Override
				public void run() {
					ViewContext[] contexts = Driver.this.viewContexts.toArray(new ViewContext[0]);
					for (int i = 0; i < contexts.length; i++) {
						contexts[i].update(timer.dt);
						if (contexts[i].contectInvalid) {
							contexts[i].content.repaint();
							contexts[i].contectInvalid = false;
						}
					}
				}
			};
			while (running) {
				timer.t = System.currentTimeMillis();
				if ((timer.dt = timer.t - timer.t0) * max_fps > 1000L) {
					fps = (int) (1000L / timer.dt);
					
					final float fdt = timer.dt / 1000.0f;
					
					Dispatcher.executeAndWaitAndSyncWithGUI(new Runnable() {
						@Override
						public void run() {
							
							// animation
							final Animation[] as = animations.toArray(new Animation[0]);
							for (int i = 0; i < as.length; i++) {
								as[i].update(fdt);
							}
							
							// rendering
							runnable.run();
							
						}
					});
					
					Thread.yield();
					timer.t0 = timer.t;
				} else {
					try {
						Thread.sleep(1000L / max_fps - timer.dt);
					} catch (InterruptedException e) {
						Thread.yield();
					}
				}
			}
		}
	}
	
	private static Driver sharedDriver = null;
	private static BufferedImage sharedSmallImage = null;
	private static GraphicsX sharedGraphics = null;
	
	static Driver getDriver() {
		if (sharedDriver == null) {
			sharedDriver = new Driver();
		}
		return sharedDriver;
	}
	
	public static GraphicsConfiguration getDefaultGraphicsConfiguration() {
		return GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
	}
	
	public static GraphicsX getSharedGraphics() {
		if (sharedSmallImage == null) {
			sharedSmallImage = getDefaultGraphicsConfiguration().createCompatibleImage(1, 1, Transparency.TRANSLUCENT);
		}
		if (sharedGraphics == null) {
			sharedGraphics = new GraphicsX(sharedSmallImage.createGraphics());
		}
		return sharedGraphics;
	}

	public static int getFPS() {
		return getDriver().fps;
	}

	public static int getFPSLimitation() {
		return getDriver().max_fps;
	}

	public static void setFPSLimitation(int fps) {
		getDriver().max_fps = fps;
	}

	//// Instance ////
	
	class BufferPool {
		long buffer_active = 0;
		long buffer_cached = 0;
		long buffer_limitation = 32 * 1024 * 1024; // 32 MB
		static private final int SQUARE = 0;		// 4:3 - 3:4
		static private final int WIDE = 1;			// 4:3 - 4:2
		static private final int NARROW = 2;		// 3:4 - 2:4
		static private final int VERY_WIDE = 3;		// 4:2 - 4:0
		static private final int VERY_NARROW = 4;	// 2:4 - 0:4
		private int getRectangleType(int width, int height) {
			width *= 12;
			if (height * 9 <= width && width <= height * 16) return SQUARE;
			else {
				height *= 12;
				if (width > height) {
					return (width >> 1) <= height ? WIDE : VERY_WIDE;
				} else {
					return (height >> 1) <= width ? NARROW : VERY_NARROW;
				}
			}
		}
		private final ArrayList<LinkedList<VolatileImage>> pools;
		BufferPool() {
			pools = new ArrayList<LinkedList<VolatileImage>>(5);
			for (int i = 0; i < 5; i++) {
				pools.add(new LinkedList<VolatileImage>());
			}
		}
		private VolatileImage requestBufferFromPool(int width, int height) {
			LinkedList<VolatileImage> pool = pools.get(getRectangleType(width, height));
			for (VolatileImage buf : pool) {
				// skip small buffer
				if (buf.getWidth() >= width && buf.getHeight() >= height) {
					// compute waste
					if (buf.getWidth() <= width * 2 && buf.getHeight() <= height * 2) {
						// hit
						pool.remove(buf);
						return buf;
					} else {
						return null;
					}
				}
			}
			return null;
		}
		VolatileImage getBuffer(int width, int height) {
			if (width <= 0) width = 1;
			if (height <= 0) height = 1;
			VolatileImage buf = requestBufferFromPool(width, height);
			if (buf == null) {
				// create new buffer
				buffer_active += width * height * 4;
				buf = configuration.createCompatibleVolatileImage(width, height, Transparency.TRANSLUCENT);
			} else {
				int delta = buf.getWidth() * buf.getHeight() * 4;
				buffer_active += delta;
				buffer_cached -= delta;
			}
			return buf;
		}
		void turnBackBuffer(VolatileImage buf) {
			int delta = buf.getWidth() * buf.getHeight() * 4;
			LinkedList<VolatileImage> pool = pools.get(getRectangleType(buf.getWidth(), buf.getHeight()));
			ListIterator<VolatileImage> it = pool.listIterator();
			while (it.hasNext()) {
				VolatileImage img = (VolatileImage) it.next();
				if (buf.getWidth() * buf.getHeight() <= img.getWidth() * img.getHeight()) {
					// insert
					it.set(buf);
					it.add(img);
					buffer_active -= delta;
					buffer_cached += delta;
					checkBufferLimitation();
					return;
				}
			}
			it.add(buf);
			buffer_active -= delta;
			buffer_cached += delta;
			checkBufferLimitation();
		}
		void checkBufferLimitation() {
			while (buffer_cached > 0 && buffer_cached + buffer_active > buffer_limitation) {
				for (LinkedList<VolatileImage> pool : pools) {
					if (pool.size() > 0) {
						VolatileImage buf = pool.removeLast();
						buffer_cached -= buf.getWidth() * buf.getHeight() * 4;
						buf.flush();
						if (buffer_cached + buffer_active <= buffer_limitation) {
							return;
						}
					}
				}
			}
		}
		void clearBuffer() {
			for (LinkedList<VolatileImage> pool : pools) {
				for (VolatileImage buf : pool) {
					buf.flush();
				}
				pool.clear();
			}
			buffer_cached = 0;
		}
	}
	
	// content
	private final Container content;
	private View rootView;
	private final GraphicsConfiguration configuration;
	private boolean contectInvalid = true;
	
	// utility
	private BufferedImage smallImage;
	private GraphicsX graphics;
	final BufferPool bufferPool;
	
	// debug mode
	private boolean debugMode = false;
	private View debugTarget = null;
	private Font debugFont = null;
	
	public ViewContext(Container container) {
		this(container, new View(0, 0, container.getWidth(), container.getHeight()));
	}
	
	public ViewContext(final Container container, final View rootView) {
		configuration = container.getGraphicsConfiguration();
		contectInvalid = true;
		bufferPool = new BufferPool();
		
		content = new Container() {
			private static final long serialVersionUID = 5004564402902047804L;
			public void paint(java.awt.Graphics g) {
				ViewContext.this.paint((Graphics2D) g);
				super.paint(g);
			}
		};
		content.setBackground(Color.black);
		content.setLocation(0, 0);
		content.setSize(container.getSize());
		content.setFocusable(true);
		container.setLayout(new BorderLayout());
		container.add(content, BorderLayout.CENTER);
		container.addComponentListener(new ComponentAdapter() {
			@Override
			public void componentResized(ComponentEvent e) {
				content.repaint();
			}
		});
		container.addComponentListener(new ComponentAdapter() {
			@Override
			public void componentResized(ComponentEvent e) {
				contentResized(content.getWidth(), content.getHeight());
			}
		});
		
		content.addKeyListener(new KeyListener() {
			@Override
			public void keyTyped(KeyEvent e) {
				fireKeyTyped(e);
			}
			@Override
			public void keyReleased(KeyEvent e) {
				fireKeyReleased(e);
			}
			@Override
			public void keyPressed(KeyEvent e) {
				fireKeyPressed(e);
			}
		});
		content.addMouseListener(new MouseListener() {
			@Override
			public void mouseReleased(MouseEvent e) {
				fireMouseReleased(e);
				fireMouseClicked(e);
				updateMouseOnViewChain(new MotionEvent(e));
			}
			
			@Override
			public void mousePressed(MouseEvent e) {
				updateMouseOnViewChain(new MotionEvent(e));
				fireMousePressed(e);
			}
			@Override
			public void mouseExited(MouseEvent e) {
				if (e.getButton() == MouseEvent.NOBUTTON) {
					updateMouseOnViewChain(new MotionEvent(e));
					setMouseOnView(new LinkedList<View>());
				}
			}
			@Override
			public void mouseEntered(MouseEvent e) {
			}
			@Override
			public void mouseClicked(MouseEvent e) {
			}
		});
		content.addMouseMotionListener(new MouseMotionListener() {
			@Override
			public void mouseMoved(MouseEvent e) {
				updateMouseOnViewChain(new MotionEvent(e));
				fireMouseMoved(e);
			}
			@Override
			public void mouseDragged(MouseEvent e) {
				fireMouseDragged(e);
			}
		});
		content.addMouseWheelListener(new MouseWheelListener() {
			@Override
			public void mouseWheelMoved(MouseWheelEvent e) {
				fireMouseWheelMoved(e);
			}
		});
		content.requestFocus();
		
		setRootView(rootView);
		getDriver().registerContext(ViewContext.this);
	}
	
	public final View getRootView() {
		return rootView;
	}

	public void setRootView(View rootView) {
		if (this.rootView != null) {
			View.setViewContext(rootView, null);
		}
		if (rootView == null) rootView = new View(0, 0, content.getWidth(), content.getHeight());
		this.rootView = rootView;
		if (rootView.getViewContext() != null) {
			throw new IllegalArgumentException("the view already has an owner context.");
		}
		View.setViewContext(rootView, this);
		contentResized(content.getWidth(), content.getHeight());
	}

	public final GraphicsConfiguration getGraphicsConfiguration() {
		return configuration;
	}

	public final Container getAWTContainer() {
		return content;
	}

	public final GraphicsX getGraphics() {
		if (smallImage == null) {
			smallImage = configuration.createCompatibleImage(1, 1, Transparency.TRANSLUCENT);
		}
		if (graphics == null) {
			graphics = new GraphicsX(smallImage.createGraphics());
		}
		return graphics;
	}

	public void prepareContent(int width, int height) {
		rootView.setSize(width, height);
		GraphicsX g = new GraphicsX(getGraphics());
		rootView.repaint(g, true);
		g.dispose();
	}

	public void prepareContent() {
		prepareContent(content.getWidth(), content.getHeight());
	}

	public final int getBufferSizeLimitation() {
		return (int) (bufferPool.buffer_limitation / 1024 / 1024);
	}

	public void setBufferSizeLimitaion(int megaByte) {
		bufferPool.buffer_limitation = megaByte * 1024L * 1024L;
	}

	public void dispose() {
		getDriver().unregisterContext(this);
		content.setVisible(false);
		Container container = content.getParent();
		container.remove(content);
	}
	
	public boolean isDebugMode() {
		return debugMode;
	}

	public void setDebugMode(boolean debugMode) {
		this.debugMode = debugMode;
		if (debugFont == null)
			debugFont = new Font(Font.MONOSPACED, Font.PLAIN, 10);
		invalid();
	}

	public View getDebugTarget() {
		return debugMode ? debugTarget : null;
	}

	public void setDebugTarget(View view) {
		if (debugMode && view != null && view.getViewContext() == this) {
			debugTarget = view;
			invalid();
		}
	}

	private void contentResized(int w, int h) {
		if (rootView != null) {
			rootView.setSize(w, h);
		}
	}
	
	private void paint(Graphics2D g) {
		if (rootView != null) {
			GraphicsX gx = new GraphicsX(g);
			rootView.repaint(gx, true);

			// debug mode
			if (debugMode && debugTarget != null) {
				g.setColor(Color.magenta);
				g.setFont(debugFont);
				g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
				
				// border
				Vector pos = debugTarget.getPositionInRootView();
				g.drawRect(pos.x, pos.y, debugTarget.getWidth() - 1, debugTarget.getHeight() - 1);
				
				// class name
				String className = null;
				if (debugTarget.getClass().isAnonymousClass()) {
					className = debugTarget.getClass().getSuperclass().getSimpleName();
				} else {
					className = debugTarget.getClass().getSimpleName();
				}
				
				// show label
				String label = String.format("%s(%d,%d,%d,%d)",
						className,
						debugTarget.getLeft(),
						debugTarget.getTop(),
						debugTarget.getWidth(),
						debugTarget.getHeight()
						);
				FontMetrics fm = g.getFontMetrics();
				int ascent = fm.getMaxAscent();
				Rectangle2D rect = fm.getStringBounds(label, g);
				g.fillRect(pos.x, pos.y, (int) rect.getWidth() + 1, (int) rect.getHeight() + 1);
				g.setColor(Color.white);
				g.drawString(label, pos.x + 1, pos.y + ascent + 1);
				
				// show memory info
				label = String.format("view_alloc_cnt = %d, active_buf = %.1f MB, cached_buf = %.1f MB",
						View.debug_view_alloc_count,
						bufferPool.buffer_active / 1024.0 / 1024.0,
						bufferPool.buffer_cached / 1024.0 / 1024.0
						);
				rect = fm.getStringBounds(label, g);
				g.setColor(Color.orange);
				g.fillRect(rootView.getWidth() - (int) rect.getWidth() - 1, rootView.getHeight() - (int) rect.getHeight() - 1, (int) rect.getWidth() + 1, (int) rect.getHeight() + 1);
				g.setColor(Color.black);
				g.drawString(label, rootView.getWidth() - (int) rect.getWidth(), rootView.getHeight() - (int) rect.getHeight() + ascent);
				
			}
		}
	}
	
	void invalid() {
		contectInvalid = true;
	}
	
	//// Keyboard Event ////
	
	private LinkedList<View> focusViewChain = new LinkedList<View>();
	
	void requestFocus(View v) {
		final LinkedList<View> newChain = new LinkedList<View>();
		View view = v;
		while (view != null) {
			newChain.addFirst(view);
			view = view.getSuperView();
		}
		for (View old_view : focusViewChain) {
			if (!newChain.contains(old_view) && old_view.isKeyboardFocusable()) old_view.lostKeyboardFocus();
		}
		for (View new_view : newChain) {
			if (!focusViewChain.contains(new_view) && new_view.isKeyboardFocusable()) new_view.gotKeyboardFocus();
		}
		focusViewChain = newChain;
	}
	
	boolean isInFocusChain(View v) {
		return focusViewChain.contains(v);
	}
	
	boolean isFocus(View v) {
		return !focusViewChain.isEmpty() && focusViewChain.getLast() == v;
	}
	
	private void fireKeyPressed(KeyEvent event) {
		final com.cocotingo.snail.KeyEvent e = new com.cocotingo.snail.KeyEvent(event);
		final View[] chain = focusViewChain.toArray(new View[focusViewChain.size()]);
		for (int i = 0; i < chain.length; i++) {
			chain[i].preKeyPressed(e);
			if (e.handled) return;
		}
		for (int i = chain.length - 1; i >= 0; i--) {
			chain[i].postKeyPressed(e);
			if (e.handled) return;
		}
	}
	
	private void fireKeyReleased(KeyEvent event) {
		final com.cocotingo.snail.KeyEvent e = new com.cocotingo.snail.KeyEvent(event);
		final View[] chain = focusViewChain.toArray(new View[focusViewChain.size()]);
		for (int i = 0; i < chain.length; i++) {
			chain[i].preKeyReleased(e);
			if (e.handled) return;
		}
		for (int i = chain.length - 1; i >= 0; i--) {
			chain[i].postKeyReleased(e);
			if (e.handled) return;
		}
	}
	
	private void fireKeyTyped(KeyEvent event) {
		final com.cocotingo.snail.KeyEvent e = new com.cocotingo.snail.KeyEvent(event);
		final View[] chain = focusViewChain.toArray(new View[focusViewChain.size()]);
		for (int i = 0; i < chain.length; i++) {
			chain[i].preKeyTyped(e);
			if (e.handled) return;
		}
		for (int i = chain.length - 1; i >= 0; i--) {
			chain[i].postKeyTyped(e);
			if (e.handled) return;
		}
	}
	
	//// Mouse Event ////
	
	private List<View> mouseOnViewChain = new ArrayList<View>();
	
	private void updateMouseOnViewChain(com.cocotingo.snail.MouseEvent e) {
		Vector p = e.getPosition(rootView);
		final List<View> newMouseOnViewChain = new ArrayList<View>();
		newMouseOnViewChain.add(rootView);
		buildMouseOnViewChain(rootView, newMouseOnViewChain, p);
		setMouseOnView(newMouseOnViewChain);
		if (debugMode) {
			View newDebugTarget = newMouseOnViewChain.get(newMouseOnViewChain.size() - 1);
			if (debugTarget != newDebugTarget) {
				debugTarget = newDebugTarget;
				invalid();
			}
		}
	}
	
	private void buildMouseOnViewChain(View view, List<View> chain, Vector p) {
		ListIterator<View> it = view.listIterator(view.count());
		while (it.hasPrevious()) {
			View child = it.previous();
			if (!child.isHidden() && child.isInsideWithTransform(p)) {
				chain.add(child);
				p.x -= child.getLeft();
				p.y -= child.getTop();
				buildMouseOnViewChain(child, chain, p);
				return;
			}
		}
	}
	
	private void setMouseOnView(List<View> new_chain) {
		for (View old_view : mouseOnViewChain) {
			if (!new_chain.contains(old_view)) old_view.mouseExited();
		}
		for (View new_view : new_chain) {
			if (!mouseOnViewChain.contains(new_view)) new_view.mouseEntered();
		}
		mouseOnViewChain = new_chain;
	}
	
	private void fireMousePressed(MouseEvent event) {
		final com.cocotingo.snail.MouseEvent e = new MotionEvent(event);
		currentMousePosition = e.getPosition(null);
		for (View view : mouseOnViewChain) {
			view.preMousePressed(e);
			if (e.handled) return;
		}
		for (int i = mouseOnViewChain.size() - 1; i >= 0; i--) {
			mouseOnViewChain.get(i).postMousePressed(e);
			if (e.handled) return;
		}
	}
	
	private void fireMouseReleased(MouseEvent event) {
		final com.cocotingo.snail.MouseEvent e = new MotionEvent(event);
		currentMousePosition = e.getPosition(null);
		for (View view : mouseOnViewChain) {
			view.preMouseReleased(e);
			if (e.handled) return;
		}
		for (int i = mouseOnViewChain.size() - 1; i >= 0; i--) {
			mouseOnViewChain.get(i).postMouseReleased(e);
			if (e.handled) return;
		}
	}
	
	private void fireMouseClicked(MouseEvent event) {
		final com.cocotingo.snail.MouseEvent e = new MotionEvent(event);
		Vector p = currentMousePosition;
		int i = 0;
		for (; i < mouseOnViewChain.size(); i++) {
			View view = mouseOnViewChain.get(i);
			p = view.transformPointFromSuperView(p);
			if (!view.isInside(p)) break;
			view.preMouseClicked(e);
			if (e.handled) return;
		}
		for (i--; i >= 0; i--) {
			mouseOnViewChain.get(i).postMouseClicked(e);
			if (e.handled) return;
		}
	}
	
	private void fireMouseMoved(MouseEvent event) {
		final com.cocotingo.snail.MouseEvent e = new MotionEvent(event);
		currentMousePosition = e.getPosition(null);
		for (View view : mouseOnViewChain) {
			view.preMouseMoved(e);
			if (e.handled) return;
		}
		for (int i = mouseOnViewChain.size() - 1; i >= 0; i--) {
			mouseOnViewChain.get(i).postMouseMoved(e);
			if (e.handled) return;
		}
	}

	private void fireMouseDragged(MouseEvent event) {
		final com.cocotingo.snail.MouseEvent e = new MotionEvent(event);
		currentMousePosition = e.getPosition(null);
		for (View view : mouseOnViewChain) {
			view.preMouseDragged(e);
			if (e.handled) return;
		}
		for (int i = mouseOnViewChain.size() - 1; i >= 0; i--) {
			mouseOnViewChain.get(i).postMouseDragged(e);
			if (e.handled) return;
		}
	}

	private void fireMouseWheelMoved(MouseWheelEvent event) {
		final com.cocotingo.snail.MouseWheelEvent e = new com.cocotingo.snail.MouseWheelEvent(event);
		for (View view : mouseOnViewChain) {
			view.preMouseWheelMoved(e);
			if (e.handled) return;
		}
		for (int i = mouseOnViewChain.size() - 1; i >= 0; i--) {
			mouseOnViewChain.get(i).postMouseWheelMoved(e);
			if (e.handled) return;
		}
	}
	
	//// Mouse Velocity ////

	private class MotionEvent extends com.cocotingo.snail.MouseEvent {
		public MotionEvent(MouseEvent AWTEvent) {
			super(AWTEvent);
		}
		@Override
		public Vector getVelocity() {
			return cachedMouseVelocity == null ? new Vector() : cachedMouseVelocity;
		}
	}
	
	private Vector currentMousePosition = null, lastMousePosition = null;
	private Vector cachedMouseVelocity = null;
	private long mouseTimer = 0;
	
	private void update(long dt) {
		mouseTimer += dt;
		if (mouseTimer >= 40) {
			if (lastMousePosition != null) {
				cachedMouseVelocity = currentMousePosition.subtract(lastMousePosition);
				cachedMouseVelocity.multipliedBy(1000.0f / mouseTimer);
			}
			lastMousePosition = currentMousePosition;
			mouseTimer = 0;
		}
	}
	
}
